﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;

using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>
    /// Converts validation errors detected by <b>BizElements</b> framework into <see cref="BusinessRule"/> objects.
    /// </summary>
    /// <remarks>Requires entries in <see cref="ITextSource"/> to automatically build errors. Configure how the errors
    /// are built through <see cref="NullErrorPhraseTextSourceKey"/> and <see cref="OutOfRangeErrorPhraseSourceKey"/>
    /// static properties.</remarks>
    public sealed class FieldErrorsToRulesConverter
    {
        #region Config.

        /// <summary><see cref="ITextSource"/> key for phrase which is appended to field caption if <b>NullError</b> occurs.</summary>
        /// <value>A string. Default is <b>_FieldErrorsToRulesConverter_NullError</b>.</value>
        /// <remarks>Can be set in the <b>appSettings</b> section of the applications configuration file.
        /// <code>
        /// <add key="BizElements.BusinessLayer.FieldErrorsToRulesConverter.NullErrorPhraseTextSourceKey" value="_FieldErrorsToRulesConverter_NullError" />
        /// </code>
        /// </remarks>
        public static readonly string NullErrorPhraseTextSourceKey = "_FieldErrorsToRulesConverter_NullError";

        /// <summary><see cref="ITextSource"/> key for phrase which is appended to field caption if <b>OutOfRangeError</b> occurs.</summary>
        /// <value>A string. Default is <b>_FieldErrorsToRulesConverter_OutOfRangeError</b>.</value>
        /// <remarks>Can be set in the <b>appSettings</b> section of the applications configuration file.
        /// <code>
        /// <add key="BizElements.BusinessLayer.FieldErrorsToRulesConverter.OutOfRangeError" value="_FieldErrorsToRulesConverter_OutOfRangeError" />
        /// </code>
        /// </remarks>
        public static readonly string OutOfRangeErrorPhraseSourceKey = "_FieldErrorsToRulesConverter_OutOfRangeError";

        static FieldErrorsToRulesConverter()
        {
            string cfgNullErrorKey = ConfigurationManager.AppSettings["BizElements.BusinessLayer.FieldErrorsToRulesConverter.NullErrorPhraseTextSourceKey"];
            if (!string.IsNullOrEmpty(cfgNullErrorKey))
                NullErrorPhraseTextSourceKey = cfgNullErrorKey;

            string cfgOutOfRangeKey = ConfigurationManager.AppSettings["BizElements.BusinessLayer.FieldErrorsToRulesConverter.OutOfRangeErrorPhraseSourceKey"];
            if (!string.IsNullOrEmpty(cfgOutOfRangeKey))
                OutOfRangeErrorPhraseSourceKey = cfgOutOfRangeKey;
        }

        #endregion

        #region Fields.

        string ruleNamePrefix;
        SortedList codesAndPhrases = new SortedList();
        SortedList fieldsAndCaptions = new SortedList();
        LevelTwoDictionary<string, FieldErrorCode, string> customErrors = new LevelTwoDictionary<string, FieldErrorCode, string>();

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of <b>FieldErrorsToRulesConverter</b> class.
        /// </summary>
        /// <param name="ruleNamePrefix"><see cref="BusinessRule.Name"/> prefix of the rules created in the converter.</param>
        internal FieldErrorsToRulesConverter(string ruleNamePrefix)
        {
            this.ruleNamePrefix = ruleNamePrefix;
        }

        /// <summary>
        /// Initializes a new instance of <b>FieldErrorsToRulesConverter</b> class.
        /// </summary>
        /// <param name="ruleNamePrefix"><see cref="BusinessRule.Name"/> prefix of the rules created in the converter.</param>
        /// <param name="phraseSource">Source of phrases required to build the <see cref="BusinessRule.Description"/> properties of the created rules.</param>
        /// <remarks>Use this constructor when you want the converter to automatically load default phrases from
        /// the text source. Constructor expects that the messages with following keys exist in the source:
        /// <list type="bullet">
        /// <item><b>FieldErrorsToRulesConverter.NullError</b></item>
        /// <item><b>FieldErrorsToRulesConverter.OutOfRangeError</b></item>
        /// </list></remarks>
        internal FieldErrorsToRulesConverter(string ruleNamePrefix, ITextSource phraseSource)
        {
            this.ruleNamePrefix = ruleNamePrefix;
            AddErrorCodePhrase(FieldErrorCode.NullError, phraseSource.GetString(NullErrorPhraseTextSourceKey));
            AddErrorCodePhrase(FieldErrorCode.OutOfRangeError, phraseSource.GetString(OutOfRangeErrorPhraseSourceKey));
        }

        #endregion

        #region AddErrorCodePhrase.

        /// <summary>
        /// Adds phrase used to build a rule which describes the specified BizElements error.
        /// </summary>
        /// <param name="code"><b>BizElements.Core.FieldErrorCode</b> value.</param>
        /// <param name="phrase">Phrase appended after the caption of the field that failed the validation.</param>
        internal void AddErrorCodePhrase(FieldErrorCode code, string phrase)
        {
            this.codesAndPhrases[code] = phrase;
        }

        #endregion

        #region AddField.

        /// <summary>
        /// Adds field and its caption required to build the rule description.
        /// </summary>
        /// <param name="field">Database field.</param>
        /// <param name="caption">Caption of the field with witch the rule description starts.</param>
        internal void AddField(IDbColumn field, string caption)
        {
            this.fieldsAndCaptions[field.ColumnName] = caption;
        }

        #endregion

        #region SetCustomRuleDescription.

        /// <summary>
        /// Sets custom rule/error description for the specified field and error code.
        /// </summary>
        /// <param name="field">Database field.</param>
        /// <param name="errorCode"><b>BizElements.Core.FieldErrorCode</b> value.</param>
        /// <param name="ruleDescription">Custom error message used if the specifed field has given error.</param>
        public void SetCustomRuleDescription(IDbColumn field, FieldErrorCode errorCode, string ruleDescription)
        {
            this.customErrors[field.GetPrefixedAlias(), errorCode] = ruleDescription;
        }

        #endregion

        #region ConvertOne.

        /// <summary>
        /// Converts a <b>BizElements.Core.FieldError</b> structure to a <see cref="BusinessRule"/>.
        /// </summary>
        /// <param name="error">Validation error detected by BizElements.</param>
        /// <returns>Business rule that describes the BizElements error.</returns>
        private BusinessRule ConvertOne(FieldError error)
        {
            string ruleName = this.ruleNamePrefix + "_" + error.Field.PropertyName + "_" + error.ErrorCode.ToString();

            string customDescription = this.customErrors[error.Field.GetPrefixedAlias(), error.ErrorCode];
            string ruleDescription = customDescription ?? GetRuleDescription(error);

            BusinessRule rule = new BusinessRule(ruleName, ruleDescription, error.Field.PropertyName);
            if (error.ErrorCode == FieldErrorCode.AllOk)
                rule.IsBroken = false;
            else
                rule.IsBroken = true;

            return rule;
        }

        private string GetRuleDescription(FieldError error)
        {
            string sentenceStart = (string)this.fieldsAndCaptions[error.Field.ColumnName];
            sentenceStart = Capitalize(sentenceStart);
            if (string.IsNullOrEmpty(sentenceStart))
                sentenceStart = error.Field.ColumnName;

            string sentenceEnd = (string)this.codesAndPhrases[error.ErrorCode];
            string ruleDescription = sentenceStart + " " + sentenceEnd;
            return ruleDescription;
        }

        private static string Capitalize(string s)
        {
            if (s == null || s == "")
                return "";
            if (s.Length == 1)
                return s.ToUpper();
            else if (s.Length > 1)
                return s.Substring(0, 1).ToUpper() + s.Substring(1);
            else
                return s;
        }

        #endregion

        #region ConvertMulti.

        /// <summary>
        /// Converts multiple <b>BizElements.Core.FieldError</b> structures to collection of <see cref="BusinessRule"/> objects.
        /// </summary>
        /// <param name="errors">Validation errors detected by BizElements.</param>
        /// <returns>Business rules that describe the BizElements errors.</returns>
        internal BusinessRuleCollection ConvertMulti(IEnumerable<FieldError> errors)
        {
            BusinessRuleCollection rules = new BusinessRuleCollection();
            foreach (FieldError error in errors)
                rules.Add(ConvertOne(error));

            return rules;
        }

        #endregion
    }
}